package redis

import (
	"github.com/gomodule/redigo/redis"
	"github.com/mna/redisc"
	"time"
)

type Pool struct {
	pool    PoolInterface
	Name    string
	timeout time.Duration
	Type    string
}

type PoolInterface interface {
	Get() redis.Conn
	Close() error
}

type Setting struct {
	Name string `mapstructure:"name"`
	// redis类型，包括redis,sentinel,cluster,codis四种
	Type string   `mapstructure:"type"`
	Addr []string `mapstructure:"addr"`
	// 密码
	Password string `mapstructure:"passwd"`
	// 数据库，默认0
	Db int `mapstructure:"db"`
	// 用于codis连接
	ZkHost []string `mapstructure:"zk_host"`
	// 用于codis连接
	ZkPath string `mapstructure:"zk_path"`
	// 最大连接数量
	MaxPoolSize int `mapstructure:"max_pool_size"`
	// 最大空闲连接数量，超过这个数值的空闲连接将会被关闭
	MaxIdle int `mapstructure:"max_idle"`
	// 空闲时间，超过这个时间的空闲连接将会被关闭
	IdleTimeout time.Duration `mapstructure:"idle_timeout"`
	// 连接等待时间
	ConnectTimeout time.Duration `mapstructure:"connect_timeout"`
	// 读写等待时间
	DoWithTimeout time.Duration `mapstructure:"do_with_timeout"`
}

// InitRedisPool 通过此方法初始化，不需要关注底层是redis cluster还是codis等
func InitRedisPool(setting Setting) *Pool {
	switch setting.Type {
	case "redis":
		return InitRedis(setting)
	case "sentinel":
		return InitRedisSentinel(setting)
	case "cluster":
		return InitRedisCluster(setting)
	case "codis":
		return InitCodis(setting)
	default:
		return InitRedis(setting)
	}
}

// InitRedis 初始化单点redis
func InitRedis(setting Setting) *Pool {
	checkSetting(&setting)

	pool := &redis.Pool{
		MaxIdle:     setting.MaxIdle,     // 池中最大空闲连接数
		MaxActive:   setting.MaxPoolSize, // 池中最大连接数,不确定可以用0（0表示自动定义）。当Wait为true时，达到最大连接数时会等待；否则直接返回errConn
		IdleTimeout: setting.IdleTimeout, // 连接关闭时间300秒
		Wait:        true,
		// 使用DialContext带计时器的连接
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp",
				setting.Addr[0],
				redis.DialConnectTimeout(setting.ConnectTimeout),
				redis.DialWriteTimeout(setting.DoWithTimeout),
				redis.DialReadTimeout(setting.DoWithTimeout),
				redis.DialPassword(setting.Password),
				redis.DialDatabase(setting.Db),
			)
		}, // 创建连接时调用的函数
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		}, // 复用连接时，会先用此函数检查此连接是否可用
	}
	// ch为池中可用剩余连接
	// active用于总共创建的连接数
	return &Pool{
		pool:    pool,
		Name:    setting.Name,
		timeout: setting.DoWithTimeout,
		Type:    "redis",
	}
}

// Close 关闭redis
func (p *Pool) Close() {
	_ = p.pool.Close()
}

func (p *Pool) Conn() redis.Conn {
	return p.pool.Get()
}

func (p *Pool) do(command string, args ...interface{}) (interface{}, error) {
	con := p.pool.Get()
	// wrapper cluster,solved thr MOVED problem
	if p.Type == "cluster" {
		retryConn, err := redisc.RetryConn(con, 2, 100*time.Millisecond)
		if err != nil {
			return nil, err
		}
		res, err := retryConn.Do(command, args...)
		_ = retryConn.Close()
		return res, err
	}

	// metric延迟监控，以及error监控
	connWithTimeout, ok := con.(redis.ConnWithTimeout) // 实际都是实现了ConnWithTimeout方法的
	if !ok || p.timeout <= 0 {
		res, err := con.Do(command, args...)
		_ = con.Close()
		return res, err
	}
	res, err := connWithTimeout.DoWithTimeout(p.timeout, command, args...)
	_ = connWithTimeout.Close()
	return res, err
}

// checkSetting 检查配置是否正确
func checkSetting(setting *Setting) {
	var errMsg interface{}
	if setting.Name == "" {
		errMsg = "redis need name"
		panic(errMsg)
	}
	if setting.Type != "codis" && len(setting.Addr) == 0 {
		errMsg = "redis need addr"
		panic(errMsg)
	}
	if setting.Type == "codis" && (len(setting.ZkHost) == 0 || setting.ZkPath == "") {
		errMsg = "codis need zk info"
		panic(errMsg)
	}

	if setting.MaxPoolSize == 0 {
		setting.MaxPoolSize = 5
	}
	if setting.MaxIdle == 0 {
		setting.MaxIdle = 2
	}
	if setting.ConnectTimeout.Nanoseconds() == 0 {
		setting.ConnectTimeout = 2 * time.Second
	} else {
		setting.ConnectTimeout *= time.Second
	}
	if setting.DoWithTimeout.Nanoseconds() == 0 {
		setting.DoWithTimeout = 2 * time.Second
	} else {
		setting.DoWithTimeout *= time.Second
	}
	if setting.IdleTimeout.Nanoseconds() == 0 {
		setting.IdleTimeout = 60 * time.Second
	} else {
		setting.IdleTimeout *= time.Second
	}
}
